/*
 * Copyright (c) 2001, 2002 Boris Pruessmann and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Common Public License v0.5
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v05.html
 * 
 * Contributors:
 *    Boris Pruessmann - Initial implementation.
 */
package net.sourceforge.perforce.core;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import net.sourceforge.perforce.core.api.IPerforceServer;
import net.sourceforge.perforce.core.api.IPerforceServerManager;
import net.sourceforge.perforce.core.internal.Policy;
import net.sourceforge.perforce.core.internal.api.PerforceServer;
import net.sourceforge.perforce.core.internal.events.ResourceStateDelta;
import net.sourceforge.perforce.core.syncinfo.FolderSyncInfo;
import net.sourceforge.perforce.core.syncinfo.IResourceStateDelta;
import net.sourceforge.perforce.core.syncinfo.ReentrantLock;
import net.sourceforge.perforce.core.syncinfo.ResourceSyncInfo;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;

/**
  * @version $Revision: 77 $
  * @author  <a href="mailto:bpruessmann@users.sourceforge.net">Boris Pruessmann</a>
  */
public class PerforceSynchronizer
{
	// --------------------------------------------------------------------------- Private Constants
	
	/** Used as identifier for folder sync info persistent property. */
	private static final QualifiedName FOLDER_SYNC_KEY = 
		new QualifiedName(PerforceCorePlugin.ID, "folder-sync");

	/** Used as identifier for resource sync ingo persistent property. */
	private static final QualifiedName RESOURCE_SYNC_KEY = 
		new QualifiedName(PerforceCorePlugin.ID, "resource-sync");
		
	/** Used as indicator for folder sync info that does not exist (in comparison to not yet
	  * fetched)
	  */
	private static final FolderSyncInfo NULL_FOLDER_SYNC_INFO =	new FolderSyncInfo("");

	/** Ok Status used during endOperation() */
	private static final IStatus STATUS_OK = 
		new Status(IStatus.OK, PerforceCorePlugin.ID, 0, Policy.bind("ok"), null);     //$NON-NLS-1$
	
	// -------------------------------------------------------------------- Private Static Variables
	
	/** Stores the PerforceSynchronizer singleton . */
	private static PerforceSynchronizer instance;
	
	// --------------------------------------------------------------------------- Private Variables
	
	/** Used to achieve reentrance. */
	private ReentrantLock lock = new ReentrantLock();
	
	private Map changedResourceSync = new HashMap();
	private Map changedFolderSync = new HashMap();
	
	// -------------------------------------------------------------------------------- Constructors
	
	/** Constructor for PerforceSynchronizer. */
	PerforceSynchronizer()
	{
	}

	// ----------------------------------------------------------------------- Public Static Methods
	
	/** Returns the PerforceSynchronizer singleton instance. 
	  * 
	  * @return the singleton instance.
	  * 
	  * @pre this.instance != null
	  */
	public static PerforceSynchronizer getInstance()
	{
		Policy.assertNotNull(instance);
		return instance;
	}
	
	/** Call this method to start the PerforceSynchronizer. Has to be performed before calling
	  * getInstance() for the first time.
	  * 
	  * @pre this.instance == null
	  */
	public static void startup()
	{
		Policy.assertNull(instance);
		instance = new PerforceSynchronizer();
	}
	
	/** Call this method for shutting down the PerforceSynchronizer. 
	  * 
	  * @pre this.instance != null
	  */
	public static void shutdown()
	{
		Policy.assertNotNull(instance);
		instance = null;
		
		// TODO:
	}
	
	// ------------------------------------------------------------------------------ Public Methods
	
	/** Begins a batch of operations.
	  * 
	  * @param monitor the progress monitor, may be null
	  */
	public void beginOperation(IProgressMonitor monitor)
	{
		lock.acquire();
		if (lock.getNestingCount() == 1)
		{
			prepareCache();
		}
	}
	
	/** Ends a batch of operations. Performs broadcast of changes to registered listeners. 
	  * 
	  * @param monitor the progress monitor, may be null.
	  * @throws PerforceException in case of an unexpected error.
	  */
	public void endOperation(IProgressMonitor monitor)
		throws PerforceException
	{
		try
		{
			IStatus status = STATUS_OK;
			if (lock.getNestingCount() == 1) 
			{				
				status = commitCache(monitor);
			}
			
			if (status != STATUS_OK && !status.isOK()) 
			{
				throw new PerforceException(status);
			}
		}
		finally
		{
			lock.release();
		}
	}
	/** Call this method to retrieve the FolderSyncInfo of the given container.
	  * 
	  * @param folder the container for which FolderSyncInfo is needed.
	  * @return the requested FolderSyncInfo or null if the folder isn't synced.
	  * 
	  * @throws PerforceException in case of an error.
	  * 
	  * @pre folder != null
	  */
	public FolderSyncInfo getFolderSync(IContainer folder)
		throws PerforceException
	{
		Policy.assertNotNull(folder);
		
		if (folder.getType() == IResource.ROOT || !folder.exists())
		{
			return null;
		}
		
		try
		{
			beginOperation(null);
			return cacheFolderSync(folder);
		}
		finally
		{
			endOperation(null);
		}
	}

	/** Sets the FolderSyncInfo of the given folder.
	  *
	  * @param folder the folder.
	  * @param syncInfo the new sync info.
	  * 
	  * @throws PerforceException in case of an unexpected error. 
	  */
	public void setFolderSync(IContainer folder, FolderSyncInfo syncInfo)
		throws PerforceException
	{
		if (folder.getType() == IResource.ROOT || !folder.exists())
		{
			return;
		}
		
		try
		{
			beginOperation(null);
			
			setCachedFolderSync(folder, syncInfo);
		}
		finally
		{
			endOperation(null);
		}
	}

	/** Deletes the FolderSyncInfo for the given container.
	  * 
	  * @param container the container.
	  * 
	  * @throws PerforceException in case of an error.
	  * 
	  * @pre container != null
	  */
	public void deleteFolderSync(IContainer container)
		throws PerforceException
	{
		if (container.getType() == IResource.ROOT || !container.exists())
		{ 
			return;
		}
		
		try
		{		
			beginOperation(null);
			
			removeFolderSyncInfo(container);
			
			cacheResourceSyncForChildren(container);
			Collection infos = getCachedResourceSyncForChildren(container);
			for (Iterator it = infos.iterator(); it.hasNext();) 
			{
				ResourceSyncInfo info = (ResourceSyncInfo) it.next();
				
				IPath path = new Path(info.getName());
				IResource resource = container.getFile(path);
				
				deleteResourceSync(resource);
			}
		}
		finally
		{
			endOperation(null);
		}
	}
		
	/** Call this method to request the ResourceSyncInfo of the given resource.
	  * 
	  * @param file the IResource for which ResourceSyncInfo is requested.
	  * @return the requested ResourceSyncInfo.
	  * 
	  * @throws PerforceException in case of an exceptional situation.
	  * 
	  * @pre file != null
	  */
	public ResourceSyncInfo getResourceSync(IResource file)
		throws PerforceException	
	{
		Policy.assertNotNull(file);		
		
		IContainer parent = file.getParent();
		if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT)
		{
			return null;
		}
		
		try
		{
			beginOperation(null);
			
			cacheResourceSyncForChildren(parent);
			return getCachedResourceSync(file);
		}
		finally
		{
			endOperation(null);
		}
	}
	
	/** Changes the ResourceSyncInfo for the given resource.
	  * 
	  * @param resource the resource to change.
	  * @param syncInfo the new ResourceSyncInfo.
	  * 
	  * @throws PerforceException in case of an error.
	  * 
	  * @pre resource != null
	  * @pre syncInfo != null
	  */
	public void setResourceSync(IResource resource, ResourceSyncInfo syncInfo)
		throws PerforceException
	{
		Policy.assertNotNull(resource);
		Policy.assertNotNull(syncInfo);
	
		IContainer parent = resource.getParent();
		if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT)
		{
			return;
		}
		
		try
		{
			beginOperation(null);
			
			cacheResourceSyncForChildren(parent);
			setCachedResourceSync(resource, syncInfo);			
		}
		finally
		{
			endOperation(null);
		}		
	}
	
	/** Deletes the ResourceSyncInfo of the given resource.
	  * 
	  * @param resource the resource.
	  * 
	  * @throws PerforceException in case of an error.
	  * 
	  * @pre container != null
	  */
	public void deleteResourceSync(IResource resource)
		throws PerforceException
	{
		Policy.assertNotNull(resource);

		IContainer parent = resource.getParent();
		if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT)
		{
			return;
		}
		
		try
		{		
			beginOperation(null);
			
			cacheResourceSyncForChildren(parent);
			setCachedResourceSync(resource, null);
		}
		finally
		{
			endOperation(null);
		}
	}
	
	// ----------------------------------------------------------------------------- Private Methods
		
	/** Removes the FolderSyncInfo of the given container.
	  * 
	  * @param container the container.
	  * 
	  * @throws PerforceException in case of an error.
	  */
	private void removeFolderSyncInfo(IContainer container)
		throws PerforceException
	{
		try
		{
			FolderSyncInfo info = (FolderSyncInfo) container.getSessionProperty(FOLDER_SYNC_KEY);
			if (info != null)
			{
				setCachedFolderSync(container, null);
			}
		}
		catch (CoreException e)
		{
			throw new PerforceException(e.getStatus());
		}
	}
	
	/** Removes the ResourceSyncInfo of the given resource.
	  * 
	  * @param file the resource.
	  * 
	  * @throws PerforceException in case of an error.
	  */
	private void removeResourceSyncInfo(IResource file)
		throws PerforceException
	{
		IContainer parent = file.getParent();
		if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT)
		{
			return;
		}
		
		cacheResourceSyncForChildren(parent);
		if (getCachedResourceSync(file) != null)
		{
			setCachedResourceSync(file, null);
		}
	}
		
	// ---------------------------------------------------------------------- Private Static Methods

	/** This method is called to cache the FolderSyncInfo for the specified container.
	  *  
	  * @param container the container.
	  * @return the FolderSyncInfo.
	  * 
	  * @throws PerforceException when attaching the persistent property fails.
	  */	
	private static FolderSyncInfo cacheFolderSync(IContainer container)
		throws PerforceException
	{
		try
		{
			FolderSyncInfo info = (FolderSyncInfo) container.getSessionProperty(FOLDER_SYNC_KEY);
			if (info == null)
			{
				IPerforceServerManager manager = PerforceCorePlugin.getServerManager();
				IPerforceServer server = manager.getPerforceServer(container);
				
				info = server.getFolderSyncInfo(container);
				if (info == null)
				{
					container.setSessionProperty(FOLDER_SYNC_KEY, NULL_FOLDER_SYNC_INFO);	
				}
				else
				{
					container.setSessionProperty(FOLDER_SYNC_KEY, info);
				}
			}
			else
			if (info == NULL_FOLDER_SYNC_INFO)
			{
				info = null;
			}

			return info;
		}
		catch (CoreException e)
		{
			throw new PerforceException(e.getStatus());
		}
	}

	/** This method is being called to cache the ResourceSyncInfo for the children of the
	  * given container.
	  * 
	  * @param container the container.
	  * 
	  * @throws PerforceException when attaching the persistent property fails.
	  */
	private static void cacheResourceSyncForChildren(IContainer container)
		throws PerforceException
	{
		try
		{
			Map children = (Map) container.getSessionProperty(RESOURCE_SYNC_KEY);
			if (children == null)			
			{
				IPerforceServerManager manager = PerforceCorePlugin.getServerManager();
				IPerforceServer server = manager.getPerforceServer(container);

				ResourceSyncInfo[] infos = server.getResourceSyncInfo(container);
				if (infos != null)
				{
					children = new HashMap(infos.length);
					for (int i = 0; i < infos.length; i++)
					{
						ResourceSyncInfo syncInfo = infos[i];
						children.put(syncInfo.getName(), syncInfo);
					}
				}
				else
				{
					children = new HashMap(0);
				}
				
				container.setSessionProperty(RESOURCE_SYNC_KEY, children);
			}			
		}
		catch (CoreException e)
		{
			throw new PerforceException(e.getStatus());
		}
	}		

	/** Sets the cached folder sync info.
	  *
	  * @param container the container.
	  * @param syncInfo the new FolderSyncInfo.
	  * @throws PerforceException in case of an unexpected error.
	  */
	private void setCachedFolderSync(IContainer container, FolderSyncInfo syncInfo)
		throws PerforceException
	{
		try
		{
			container.setSessionProperty(FOLDER_SYNC_KEY, syncInfo);
			
			IPerforceServerManager manager = PerforceCorePlugin.getServerManager();
			IPerforceServer server = manager.getPerforceServer(container);

			changedFolderSync.put(container, syncInfo);
		}
		catch (CoreException e)
		{
			throw new PerforceException(e.getStatus());
		}
	}

	/** Method is called to retrieve the cached ResourceSyncInfo for the given resource.
	  * 
	  * @param resource the resource.
	  * @return the ResoruceSyncInfo.
	  * 
	  * @throws PerforceException when reading of the persistent property fails.
	  */
	private static ResourceSyncInfo getCachedResourceSync(IResource resource)
		throws PerforceException
	{
		try
		{
			IContainer parent = resource.getParent();
			Map children = (Map) parent.getSessionProperty(RESOURCE_SYNC_KEY);
			Policy.assertNotNull(children);
			
			return (ResourceSyncInfo) children.get(resource.getName());
		}
		catch (CoreException e)
		{
			throw new PerforceException(e.getStatus());
		}
	}
	
	/** Called to change the chached ResourceSyncInfo of the given resource.
	  * 
	  * @param resource the resource.
	  * @param syncInfo the new ResourceSyncInfo.
	  * 
	  * @throws PerforceException if attaching the new persistent property fails.
	  */
	private void setCachedResourceSync(IResource resource, ResourceSyncInfo syncInfo)
		throws PerforceException
	{
		try
		{
			IContainer parent = resource.getParent();
			Map children = (Map) parent.getSessionProperty(RESOURCE_SYNC_KEY);
			Policy.assertNotNull(children);

			if (syncInfo == null)
			{
				children.remove(resource.getName());
			}			
			else
			{
				children.put(resource.getName(), syncInfo);
			}
			
			changedResourceSync.put(resource, syncInfo);			
		}		
		catch (CoreException e)
		{
			throw new PerforceException(e.getStatus());
		}	
	}
	
	/**
	  * Returns the resource sync info for all children of the container.
	  * Container must exist and must not be the workspace root.
	  * The resource sync info for the children of the container MUST ALREADY BE CACHED.
	  * 
	  * @param container the container
	  * @return a collection of the resource sync info's for all children
	  * @see #cacheResourceSyncForChildren
	  * 
	  * @throws PerforceException in case of an unexpected error.
	  */
	private static Collection getCachedResourceSyncForChildren(IContainer container) 
		throws PerforceException 
	{
		try 
		{
			Map children = (Map) container.getSessionProperty(RESOURCE_SYNC_KEY);
			if (children == null) 
			{
				Policy.assertTrue(false);
			}
			
			return new HashSet(children.values());
		} 
		catch (CoreException e) 
		{
			throw new PerforceException(e.getStatus());
		}
	}
	

	// ----------------------------------------------------------------------------- Private Methods

	/** Prepares the cache. */
	private void prepareCache() 
	{
		changedResourceSync.clear();
		changedFolderSync.clear();
	}
	
	/** Commit changes to the cache. 
	  * 
	  * @param monitor for progress feedback.
	  * 
	  * @return the status of the operation.
	  */
	private IStatus commitCache(IProgressMonitor monitor)
	{
		IStatus result = STATUS_OK;
				
		List resourceStateDeltas = new LinkedList();
		try
		{
			monitor = Policy.monitorFor(monitor);
			IPerforceServerManager manager = PerforceCorePlugin.getServerManager();
			
			int count = 0;
			int total = changedFolderSync.size() + changedResourceSync.size();
			for (Iterator it = changedFolderSync.entrySet().iterator(); it.hasNext();)
			{
				Map.Entry entry = (Map.Entry) it.next();

				monitor.subTask("Refreshing cache... [" + (++count) + "/" + total +"]");
				
				IContainer container = (IContainer) entry.getKey();
				FolderSyncInfo syncInfo = (FolderSyncInfo) entry.getValue();
				
				IPerforceServer server = manager.getPerforceServer(container);
				FolderSyncInfo oldSyncInfo = 
					((PerforceServer) server).setFolderSyncInfo(container, syncInfo);
					
				int kind = IResourceStateDelta.CHANGED;
				if (oldSyncInfo == null)
				{
					kind = IResourceStateDelta.ADDED;
				}
				else
				if (syncInfo == null)
				{
					kind = IResourceStateDelta.REMOVED;
				}
				
				if (syncInfo != null || oldSyncInfo != null)
				{
					resourceStateDeltas.add(new ResourceStateDelta(kind, container, null, null));
				}
			}
			
			for (Iterator it = changedResourceSync.entrySet().iterator(); it.hasNext();)
			{
				Map.Entry entry = (Map.Entry) it.next();
				
				monitor.subTask("Refreshing cache... [" + (++count) + "/" + total +"]");
				
				IResource resource = (IResource) entry.getKey();
				ResourceSyncInfo syncInfo = (ResourceSyncInfo) entry.getValue();
				
				IPerforceServer server = manager.getPerforceServer(resource);
				ResourceSyncInfo oldSyncInfo = server.setResourceSyncInfo(resource, syncInfo);
				
				int kind = IResourceStateDelta.CHANGED;
				if (oldSyncInfo == null)
				{
					kind = IResourceStateDelta.ADDED;
				}
				else
				if (syncInfo == null)
				{
					kind = IResourceStateDelta.REMOVED;
				}

				if (syncInfo != null || oldSyncInfo != null)
				{
					resourceStateDeltas.add(
						new ResourceStateDelta(kind, resource, syncInfo, oldSyncInfo));
				}
			}
		}
		catch (PerforceException e)
		{
			result = e.getStatus();
		}
		
		// Broadcast changes
		if (resourceStateDeltas.size() > 0)
		{
			IResourceStateDelta[] deltas = new IResourceStateDelta[resourceStateDeltas.size()];
			deltas = (IResourceStateDelta[]) resourceStateDeltas.toArray(deltas);
		
			return PerforceCorePlugin.getPlugin().broadcastResourceStateChanges(deltas);
		}
		
		return result;
	}	
}
